package main

import (
	"fmt"
	"strings"
	"strconv"
	"sync"
	"syscall"
	"os"
	"os/exec"
	"path/filepath"
)

func scpDirectoryDoesNotExist(config *Configuration, create_directory bool) bool {
	check_scp_directory := exec.Command(
		"ssh",
		// specify port
		"-p", strconv.Itoa(config.ssh_port),
		// specify identity file
		"-i", config.ssh_identity_path,
		// specify user@host
		fmt.Sprintf("%s@%s", config.ssh_user, config.url),
		// specify remote command
		fmt.Sprintf("test -e %s", config.scp_directory),
	)

	// start command
	err := check_scp_directory.Start()
	if err != nil {
		fmt.Fprintf(os.Stderr, "fatal error: ssh command could not be started\n")
		// this error is fatal (partly also because basically everything makes use of ssh)
		os.Exit(EXIT_SSH_ERROR)
	}

	// use wait to synchronize with command and get exit code (as err)
	does_not_exist := check_scp_directory.Wait() != nil

	if create_directory && does_not_exist {
		fmt.Printf("scp target directory %s does not exist, will be created\n", config.scp_directory)
		create_scp_directory := exec.Command(
			"ssh",
			// specify port
			"-p", strconv.Itoa(config.ssh_port),
			// specify identity file
			"-i", config.ssh_identity_path,
			// specify user@host
			fmt.Sprintf("%s@%s", config.ssh_user, config.url),
			// specify remote command
			fmt.Sprintf("mkdir -p %s", config.scp_directory),
		)
		err = create_scp_directory.Run()
		if err != nil {
			fmt.Fprintf(os.Stderr, "scp target directory %s could not be created\n", config.scp_directory)
			os.Exit(EXIT_SCP_ERROR)
		}
	}

	// signal if scp target directory does not exists
	return does_not_exist
}

func list(config *Configuration, files []string, placeholder bool) {
	if scpDirectoryDoesNotExist(config, false) {
		fmt.Fprintf(os.Stderr, "fatal error: remote directory %s does not exist\n", config.scp_directory)
		return
	}

	// get parameters
	target := fmt.Sprintf(config.url_fmt, config.url)

	// define commands
	//curl -s "${url}" | grep href | sed -e 's/<[^<]*>//g'
	cmd_curl := exec.Command("curl", "-s", target)
	cmd_grep := exec.Command("grep", "href")
	cmd_sed := exec.Command("sed", "-e", "s/<[^<]*>//g")

	// pipe curl into grep
	pipe_curl_to_grep, err := cmd_curl.StdoutPipe()
	if err != nil {
		fmt.Fprintf(os.Stderr, "fatal error: construction of pipe failed\n")
		// this error is fatal
		return
	}

	// make sure to close pipe at the end (if it was opened)
	defer pipe_curl_to_grep.Close()
	// finalize pipe
	cmd_grep.Stdin = pipe_curl_to_grep

	// pipe grep into sed
	pipe_grep_to_sed, err := cmd_grep.StdoutPipe()
	if err != nil {
		fmt.Fprintf(os.Stderr, "fatal error: construction of pipe failed\n")
		return
	}

	// make sure to close pipe at the end (if it was opened)
	defer pipe_grep_to_sed.Close()
	// finalize pipe
	cmd_sed.Stdin = pipe_grep_to_sed

	// let both initial commands do their work
	cmd_curl.Start()
	cmd_grep.Start()

	// finally query stdout/stderr of last command
	stdoutstderr, err := cmd_sed.CombinedOutput()
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: getting of output by `sed` failed\n")
	}

	// give it to our user
	fmt.Printf("%s", stdoutstderr)
}

func pull(config *Configuration, filenames []string, save bool) {
	// require at least one file
	if len(filenames) <= 0 {
		fmt.Printf("pull command requires at least one file\n")
		return
	}

	// NOTE: this function does allow duplicate filenames, which, quite magically, still works

	// check existence of directory on remote host
	if scpDirectoryDoesNotExist(config, false) {
		fmt.Fprintf(os.Stderr, "fatal error: remote directory %s does not exist\n", config.scp_directory)
		return
	}

	// do this now so it is already done once we get to the files
	url := fmt.Sprintf(config.url_fmt, config.url)

	// set up saving of files (instead of stdout output)
	single_file := len(filenames) <= 1
	save_files := save || !single_file

	// configure wait group for synchronization
	wg := sync.WaitGroup{}
	wg.Add(len(filenames))

	// iterate all filenames for pull action
	for _, f := range filenames {
		// skip problemativ filenames
		if strings.Contains(f, "/") {
			fmt.Printf("directories are not supported at the time\n")
			// finish task pre-emptively
			wg.Done()
			continue
		}

		// use goroutines because tasks are independent
		go func(wg *sync.WaitGroup, f string) {
			// finish task at the end
			defer wg.Done()

			// because this is a naive reimplementation I'm not going to use the actual libraries here
			// OUTDATED
    			//curl -s "${url}${1}" | base64 -d | openssl enc -d -aes-256-cbc -k "${secret}" > "${file}"
			// NEW COMMAND
    			//curl -s "${url}${1}" | openssl enc -d -aes-256-cbc -k "${secret}" --base64 > "${file}"
			cmd_curl := exec.Command("curl", "-s", fmt.Sprintf("%s%s", url, f))
			cmd_openssl := exec.Command("openssl", "enc", "-d", "-aes-256-cbc", "-base64", "-k", config.secret)

			pipe_curl_to_openssl, err := cmd_curl.StdoutPipe()
			// pipe curl into grep
			if err != nil {
				fmt.Fprintf(os.Stderr, "fatal error: construction of pipe failed\n")
				// this error is fatal
				return
			}

			// make sure to close pipe at the end (if it was opened)
			defer pipe_curl_to_openssl.Close()
			// finalize pipe
			cmd_openssl.Stdin = pipe_curl_to_openssl

			// set up command output
			cmd_openssl.Stdout = os.Stdout
			if save_files {
				// create file in preparation to execute command
				file, err := os.Create(f)
				defer file.Close()
				// handle error if no such file exists exists on remote host
				if err != nil {
					fmt.Fprintf(os.Stderr, "cannot create file %s: %s\n", f, err.Error())
				}

				// assign stdout to newly created file
				cmd_openssl.Stdout = file
			}

			// let both initial commands do their work
			cmd_curl.Start()

			// finally let the actual command do it's work
			cmd_openssl.Run()

			// finally query stdout/stderr of last command
			if err != nil {
				fmt.Fprintf(os.Stderr, "file %s could not be pulled\n", f)
				return
			} else {
				if save_files {
					fmt.Printf("successfully pulled %s\n", f)
				}
			}
		}(&wg, f)
	}

	// synchronize (program has to wait for goroutines otherwise output will be lost)
	wg.Wait()
}

func push(config *Configuration, filenames []string, placeholder bool) {
	// require at least one file
	if len(filenames) <= 0 {
		fmt.Printf("pull command requires at least one file\n")
		return
	}

	// check for existence of directory on remote host and create if necessary
	scpDirectoryDoesNotExist(config, true)

	// configure wait group for synchronization
	wg := sync.WaitGroup{}
	wg.Add(len(filenames))

	// iterate all filenames for pull action
	for _, f := range filenames {
		// skip problemativ filenames
		if strings.Contains(f, "/") {
			fmt.Printf("directories are not supported at the time\n")
			// finish task pre-emptively
			wg.Done()
			continue
		}

		// use goroutines because tasks are independent
		go func(wg *sync.WaitGroup, f string) {
			// finish task at the end
			defer wg.Done()

			// because this is a naive reimplementation I'm not going to use the actual libraries here
			// this one is more complicated as it's composed of two commands
			// OUTDATED
			//openssl enc -aes-256-cbc -k "${secret}" < "${1}" | base64 > "${tmp}"
			//scp "${port}" -i "${identity}" "${tmp}" "${user}"@"${webserver}":~/dropzone/"${file}"
			// NEW COMMAND
			//openssl enc -aes-256-cbc -base64 -k "${secret}" -in "${1}" -out "${tmp}"
			//scp "${port}" -i "${identity}" "${tmp}" "${user}"@"${webserver}":~/dropzone/"${file}"

			// create temporary file to put encrypted contents in and later push
			tmp, err := os.CreateTemp("/tmp", "strlst.*")
			if err != nil {
				fmt.Fprintf(os.Stderr, "error creating temporary file while trying to push %s\n", f)
				return
			}

			// make sure clean up happens afterwards
			defer os.Remove(tmp.Name())

			// define openssl command
			cmd_openssl := exec.Command(
				// openssl command with parameters
				"openssl", "enc", "-aes-256-cbc", "-base64",
				// specify encryption secret
				"-k", config.secret,
				// specify input file
				"-in", f,
				// specify output file
				"-out", tmp.Name(),
			)

			// start openssl command
			err = cmd_openssl.Start()
			if err != nil {
				fmt.Fprintf(os.Stderr, "error starting openssl command while trying to push %s\n", f)
				return
			}

			// wait and get exit code
			err = cmd_openssl.Wait()
			if err != nil {
				fmt.Fprintf(os.Stderr, "error executing openssl command while trying to push %s: %v\n", f, err)
				return
			}

			// define scp command
			cmd_scp := exec.Command(
				"scp",
				// specify port
				"-P", strconv.Itoa(config.ssh_port),
				// specify identity file
				"-i", config.ssh_identity_path,
				// temporary file that was created earlier
				tmp.Name(),
				// specify user, url and destination location
				fmt.Sprintf("%s@%s:%s", config.ssh_user, config.url, filepath.Join(config.scp_directory, f)),
			)

			// start scp command
			err = cmd_scp.Start()
			if err != nil {
				fmt.Fprintf(os.Stderr, "error starting scp command while trying to push %s\n", f)
				return
			}

			// wait and get exit code
			err = cmd_scp.Wait()
			if err != nil {
				fmt.Fprintf(os.Stderr, "error executing scp command while trying to push %s: %v\n", f, err)
				return
			} else {
				fmt.Printf("successfully pushed %s\n", f)
			}
		}(&wg, f)
	}

	// synchronize (program has to wait for goroutines otherwise output will be lost)
	wg.Wait()

	// at last, commit changes
	commit(config, filenames, placeholder)
}

func commit(config *Configuration, filenames []string, placeholder bool) {
	// try to make changes stick (in a git history)
	commit_changes := exec.Command(
		"ssh",
		// specify port
		"-p", strconv.Itoa(config.ssh_port),
		// specify identity file
		"-i", config.ssh_identity_path,
		// specify user@host
		fmt.Sprintf("%s@%s", config.ssh_user, config.url),
		// specify remote command
		"/home/strlst/rpi-server-config/commit-updates",
	)

	// start command and report error (if there is one)
	err := commit_changes.Start()
	if err != nil {
		fmt.Fprintf(os.Stderr, "error starting ssh command: %v\n", err)
		return
	}

	// then call wait to extract exit code (which encodes important information in this case)
	err = commit_changes.Wait()
	if err != nil {
		// big ugly nested ifs to extract an exit code, the go way (tm)
		if exit_err, ok := err.(*exec.ExitError); ok {
			if status, ok := exit_err.Sys().(syscall.WaitStatus); ok {
				if status.ExitStatus() == 9 {
					fmt.Printf("nothing to commit\n")
					return
				}
			}
		}

		// ugly unconditional execution path due to nested ifs
		fmt.Fprintf(os.Stderr, "error finalizing changes by committing updates: %v\n", err)
		return
	} else {
		fmt.Printf("committed changes\n")
	}
}

// technically the map could be modified at runtime, making for exciting possibilities
type fn func (*Configuration, []string, bool)
var COMMANDS map[string]fn = map[string]fn {
	"list": list,
	"pull": pull,
	"push": push,
	"commit": commit,
}

func IsImplemented(command string) bool {
	if _, ok := COMMANDS[command]; ok {
		return true;
	} else {
		return false;
	}
}

func Exec(command string, config *Configuration, filenames []string, save bool) {
	COMMANDS[command](config, filenames, save)
}